module ActiveBackground
  class Job
    DRbUndumped
    
    include ActiveBackground::DatabaseHandler
    
    @@job_options = {
                    :terminate => true,
                    :archive => false,
                    :postpone_termination => false,
                    :before_job => false,
                    :after_job => false,
                    :cleanup => false,
                    :archive => true,
                    :ttl => nil
                    }
    
    # 
    # Arguments:
    #   <tt>name</tt>: Name of the job (doesn't have to be uniq but similar jobs should get the same name)j
    #   <tt>options</tt>: Options that configure the internal workings of the job (you can only pass these on initiation)
    #   <tt>args</tt>: Arguments are passed to the method process_job
    #   
    # Options:
    #   <tt>:terminate</tt>: Specify if job should be terminated when processed (default true)
    #   <tt>:postpone_termination</tt>: Time in seconds to post pone termination (default false)
    #   <tt>:before_job</tt>: call before_job method before process_job (default false)
    #   <tt>:after_job</tt>: call after_job method after process_job (default false)
    #   <tt>:cleanup</tt>: call cleanup after everything is done (default false)
    #   <tt>:archive</tt>: archive the job (default true)
    #   <tt>:ttl</tt>: time in seconds to wait before terminating job (:infinitive to never terminate the job)
    #   <tt>:kill_on_terminate</tt>: skip archive and postpone_termination
    #   
    def initialize(name, options={}, *args)
      @job_status = :created
      @job_lock = Mutex.new
      @job_key = options[:key] || gen_key
      @job_args = args
      @job_statistics = { :created_on => Time.new }
      @job_progress = 0
      @job_name = name
      @job_terminated = false
      @job_options = @@job_options.merge(options)
      @job_result_valid = true
      @job = nil
      
      unless @job_options[:ttl] == :infinitive or @job_options[:ttl] == nil
        @job_ttl_thread = Thread.new do
          sleep @job_options[:ttl]
          terminate
        end
      end
    end
     
    def process
      return false unless @job_status == :created or @job_status == :infinitive
      @job_status=:started
      ACTIVE_BACKGROUND_LOGGER.info('* Job ' + self.class.to_s + ' started')
      
      begin
        # give the worker the chance to run procedures before starting work
        # the options is automaticly set if before_work exists can be
        # overwritten by manualy setting it to true
        if @job_options[:before_job] and !terminated?
          ACTIVE_BACKGROUND_LOGGER.debug('job ' + key + ' is preparing with before_work')
          before_job(args)
        end
        
        # start the job
        unless terminated?
          @job_status=:running
          @job_statistics[:started_on] = Time.new
          ACTIVE_BACKGROUND_LOGGER.info('* Job ' + self.class.to_s + ' processing')
          process_job
          @job_status=:processed
        end
        
        # this method is used to write the results to database or pass
        # them on to an other application (by webservice)
        if @job_options[:after_job] and !terminated?
          @job_statistics[:processed_on] = Time.new
          ACTIVE_BACKGROUND_LOGGER.debug('* Job ' + key + ' is finishing processing')
          after_job
        end
        
        # allows to validate the results, default true is passed but you can
        # reimplement the method to allow checking if everything went o.k.
        unless valid? or terminated?
          ACTIVE_BACKGROUND_LOGGER.debug('* Job ' + key + ' results are invalid')
          no_valid_results
        end
        
        # this method allows you to cleanup things, most of the time you will
        # do the cleanup by using after_work, but if you have multiple methods
        # registerd and you don't know which method will run last you can use
        # cleanup to cleanup after running all after_work methods.
        if @job_options[:cleanup] and !terminated?
          ACTIVE_BACKGROUND_LOGGER.debug('* Job ' + key + ' is cleaning up')
          cleanup
        end
        
        if @job_options[:ttl] == :infinitive
          # TODO: integrate better, count run(s) and integrate a sleep pattern
          @job_status = :infinitive
          process
        else
          @job_statistics[:terminated_on] = Time.new
          terminate # we are done with working, lets mark this job for termination
        end
      rescue Exception => e
        ACTIVE_BACKGROUND_LOGGER.debug(e.to_s + "\n" + e.backtrace.join("\n"))
        terminate
      end
    end
    
    def process_job
      unless @job
        ACTIVE_BACKGROUND_LOGGER.warn('You need to implement process_job')
      else
        self.instance_eval(&@job)
      end
    end
    
    def define_job(&job)
      @job = job
    end

    def terminate
      @job_options[:ttl]=nil
      Thread.new do
        initiate_termination if @job_options[:terminate]
      end
    end
    
    def initiate_termination
      sleep @job_options[:postpone_termination] if @job_options[:postpone_termination]
      archive
      # TODO: Need to cleanup the job
      @job_status = :terminated
      @job_terminated = true
      ACTIVE_BACKGROUND_LOGGER.debug('job ' + key.to_s + ' is terminated')
    end

    def terminated?
      @job_terminated
    end
    
    def archive
      # TODO: write results, options and arguments to file unless specified different
    end
    
    def recover_from_archive
      # TODO: recover results, options and arguments from file and provide a interface
    end
    
    def name
      @job_name
    end
    
    def key
      @job_key
    end
    alias :job_key :key
    
    def progress
      @job_progress
    end
    alias :job_progress :progress
    
    def status
      @job_status
    end
    alias :job_status :status
    
    def status=(status)
      @job_status=status
    end
    alias :job_status= :status=
    
    def args
      @job_args
    end
    alias :job_args :args
    
    def statistics
      @job_statistics
    end
    alias :job_statistics :statistics
    
    def statistic(key)
      @job_statistics[key]
    end
    alias :job_statistic :statistic
    
    def dump
      ACTIVE_BACKGROUND_LOGGER.info('You need to implement dump')
    end
    
    # check if the work that has been done is valid.
    # If the result is not valid the results are logged
    # and no_valid_results is called.
    def valid?
      @job_result_valid
    end
    
    def validate
      ACTIVE_BACKGROUND_LOGGER.info('You need to implement validate')
    end
    
    def valid
      @job_result_valid
    end
    alias :job_valid :valid
    
    def valid=(status)
      @job_result_valid=status
    end
    alias :job_valid= :valid=
    
    # you can override this method to dump the failed results into
    # a database or log. But most of the time you just want to know
    # that it failed and why it failed so you can report this back.
    def no_valid_results()
    end
    
    def stop
      Thread.current.stop
    end
    
    def start
      Thread.current.start
    end
            
    private
      def gen_key
        key = Digest::MD5.hexdigest("#{inspect}#{Time.now}#{rand}")
      end
  end
end